Test Hook
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 709 of xUnit Test Patterns for the latest information.
How do we design the system under test (SUT) so that we can replace its dependencies at run time?
We modify the SUT to behave differently during the test.
Sketch Test Hook embedded from Test Hook.gif
Almost every piece of code depends on some other classes, objects, modules or procedures. To unit test a piece of code properly, we would like to isolate it from its dependencies. This is hard to do if those dependencies are hard-coded within the code in the form of literal class names.
public String getCurrentTimeAsHtmlFragment() { Calendar currentTime; try { currentTime = new DefaultTimeProvider().getTime(); } catch (Exception e) { return e.getMessage(); } // etc.} Example HardCodedDependency embedded from java/com/clrstream/ex7/problem/TimeDisplay.java
Test Hook is a "method of last resort" for introducing test-specific behavior during automated testing.
How It Works
We modify the behavior of the SUT to support testing by putting a hook right into the SUT or into a depended-on component (DOC). This implies some kind of testing flag that can be checked in the appropriate place.
When To Use It
Sometimes it is appropriate to use this "pattern of last resort" when we cannot use either Dependency Injection (page X) or Dependency Lookup (page X). We use it because we have no other way to address the Untested Code (see Production Bugs on page X) caused by a Hard-Coded Dependency (see Hard to Test Code on page X).
Test Hook may be the only way to introduce Test Double (page X) behavior when we are programming in a procedural language that does not support objects, function pointers or any other form of dynamic binding.
Test Hooks can be used as a transitionary strategy to get legacy code under test. We can introduce testability using the Test Hooks and then use those Tests as Safety Net (see Goals of Test Automation on page X) while we refactor for even more testability. At some point we should be able to discard the initial round of tests that required the Test Hooks because we have enough "modern" tests to protect us.
Implementation Notes
The essence of Test Hook is to insert some code into the SUT that lets us test. Regardless of how we insert this code into the SUT, the code itself can either:
- divert control to a Test Double instead of the real object, or
- be the Test Double within the real object, or
- be a test-specific Decorator[GOF] that delegates to the the real object when in production.
The flag that indicates testing is in progress can be a compile time constant which may cause the compiler to optimize out all the testing logic. In languages that support preprocessors or compiler macros, these can also be used to remove the Test Hook before going into production. The flag can also be read in from configuration data or stored in a global variable that can be set directly by the test.
Motivating Example
This is an example of a test which cannot be made to pass "as is":
public void testDisplayCurrentTime_AtMidnight() { // fixture setup TimeDisplay sut = new TimeDisplay(); // exercise sut String result = sut.getCurrentTimeAsHtmlFragment(); // verify direct output String expectedTimeString = "<span class=\"tinyBoldText\">Midnight</span>"; assertEquals( expectedTimeString, result); } Example NondeterministicTest embedded from java/com/clrstream/ex7/test/TimeDisplayTest.java
This test almost always fails because it depends on the current time being returned to the SUT by a depended-on component. The values being returned by that component, the DefaultTimeProvider, cannot be controlled by the test. Therefore, this test will only pass when the system time is exactly midnight.
public String getCurrentTimeAsHtmlFragment() { Calendar currentTime; try { currentTime = new DefaultTimeProvider().getTime(); } catch (Exception e) { return e.getMessage(); } // etc.} Example HardCodedDependency embedded from java/com/clrstream/ex7/problem/TimeDisplay.java
Because the SUT is hard-coded to use a particular class to retrieve the time, there is no way to replace the depended-on component with a Test Double. That makes this test nondeterministic and pretty much useless. We need to find a way to get control of the indirect inputs of the SUT.
Refactoring Notes
We can introduce a Test Hook by creating a flag that can be checked in the SUT. We then wrap the production code with an if/then/else control structure and put the test-specific logic into the then clause.
Example: Test Hook in System Under Test
Here's the production code modifed to accomodate testing via a Test Hook:
public String getCurrentTimeAsHtmlFragment() { Calendar theTime; try { if (TESTING) { theTime = new GregorianCalendar(); theTime.set(Calendar.HOUR_OF_DAY, 0); theTime.set(Calendar.MINUTE, 0);} else { theTime = new DefaultTimeProvider().getTime(); } } catch (Exception e) { return e.getMessage(); } // etc. Example TestHookInSUT embedded from java/com/xunitpatterns/dft/lookup/HookedTimeDisplay.java
In this case we have implemented the testing flag as global constant that can be edited. This implies a separate build for versions of the system to be tested. This is somewhat safer than using a dynamic configuration parameter or member variable because many compilers will optimize this hook right out of the object code.
Example: Test Hook in Depended-on Component
We can also introduce a Test Hook putting the hook into a DOC rather than into the SUT.
public Calendar getTime() throws TimeProviderEx { Calendar theTime = new GregorianCalendar(); if (TESTING) { theTime.set(Calendar.HOUR_OF_DAY, 0); theTime.set(Calendar.MINUTE, 0);} else { // just return the calendar } return theTime; }; Example TestHookInDOC embedded from java/com/xunitpatterns/dft/lookup/HookedTimeProvider.java
This is somewhat better because we are not modifying the SUT as we test it.
Copyright © 2003-2008 Gerard Meszaros all rights reserved